package yuque

import (
	"context"
	"crypto/md5"
	"encoding/json"
	"errors"
	"fmt"
	webEntity "go-yuque/entity"
	"go-yuque/logger"
	"go-yuque/tokenBucket"
	"go-yuque/yuque/entity"
	"go-yuque/yuque/util"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

func init() {
	go func() {
		// 每五秒执行一次
		// 清除过期的令牌桶
		ticker := time.NewTicker(5 * time.Second)
		for {
			select {
			case <-ticker.C:
				logger.Debugf("[tokenBucketMap]开始扫描")
				// 获取令牌桶的创建时间
				for k, v := range tokenBucketMap {
					lastIdx := strings.LastIndex(k, "_")
					if lastIdx <= 0 {
						continue
					}
					timestampStr := k[lastIdx+1:]
					// 字符串转数字
					timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
					if err != nil {
						logger.Errorf("[tokenBucketMap]字符串转数字失败,[%s],%v", timestampStr, err)
						continue
					}
					// 获取20分钟前的时间戳
					expiredTimeStamp := time.Now().Add(-1 * time.Minute).Unix()

					if timestamp < expiredTimeStamp {
						logger.Infof("[tokenBucketMap]令牌桶已过期,key:%s", k)
						v.Close()
						delete(tokenBucketMap, k)
					}
				}
				logger.Debugf("[tokenBucketMap]扫描结束")
			}
		}
	}()
}

// GetUserAllCategoryBooks 获取登录用户所有知识库
func GetUserAllCategoryBooks(yqCookie string) (categoryBooksPt *entity.YuqueCategoryBooksStruct, err error) {

	yqUrl := "https://www.yuque.com/api/mine/book_stacks"

	browser := &util.FakeBrowserStruct{
		Url: yqUrl,
		Headers: http.Header{
			"Cookie": []string{yqCookie},
		},
	}

	respBytes, err := browser.DoGet()
	if err != nil {
		logger.Errorf("[GetUserAllCategoryBooks]获取所有知识库失败,err:%v", err)
		return
	}
	logger.Debugf("[GetUserAllCategoryBooks]resp:%s", string(respBytes))

	categoryBooksPt = &entity.YuqueCategoryBooksStruct{}

	err = json.Unmarshal(respBytes, categoryBooksPt)
	if (*categoryBooksPt).Status != 0 && err == nil {
		err = fmt.Errorf("获取所有知识库失败,resp:%s", string(respBytes))
		return
	}
	if err != nil {
		logger.Errorf("[GetUserAllCategoryBooks]json解析异常,err:%v", err)
		return
	}

	logger.Infof("[GetUserAllCategoryBooks]获取所有知识库成功")
	logger.Debugf("[GetUserAllCategoryBooks]categoryBooks:%+v", *categoryBooksPt)

	return
}

// GetAllDocsInBook 获取知识库中所有文档
func GetAllDocsInBook(loginUser, slug, bookIdx, yqCookie string) (resultArr []*entity.TocStruct, bookTreeStr string, err error) {
	if len(loginUser) <= 0 || len(slug) <= 0 || len(yqCookie) <= 0 {
		return
	}

	// 单个知识库URL: https://www.yuque.com/{loginUser}/{slug}
	docsUrl := fmt.Sprintf("https://www.yuque.com/%s/%s", loginUser, slug)
	fakeBrowser := &util.FakeBrowserStruct{
		Url: docsUrl,
		Headers: http.Header{
			"Cookie": []string{yqCookie},
		},
	}

	respBytes, err := fakeBrowser.DoGet()
	if err != nil {
		logger.Errorf("[GetAllDocsInBook]获取知识库文档失败,err:%v", err)
		return
	}

	compile, err := regexp.Compile("JSON.parse\\(decodeURIComponent\\(\"(.*)\"\\)\\);")
	if err != nil {
		logger.Errorf("[GetAllDocsInBook]正则异常,err:%v", err)
		return
	}
	submatch := compile.FindSubmatch(respBytes)
	if len(submatch) <= 1 {
		logger.Errorf("[GetAllDocsInBook]正则匹配失败")
		err = fmt.Errorf("正则匹配失败")
		return
	}

	unescapeJson, err := url.QueryUnescape(string(submatch[1]))
	if err != nil {
		logger.Errorf("[GetAllDocsInBook]url解码异常,err:%v", err)
		return
	}

	book := &entity.YuqueBookStruct{}
	err = json.Unmarshal([]byte(unescapeJson), book)
	if err != nil {
		logger.Errorf("[GetAllDocsInBook]json解析异常,err:%v", err)
		return
	}

	rootNodePt, bookTreeStr, err := ParseBook(book)
	if err != nil {
		logger.Errorf("[GetAllDocsInBook]解析知识库文档异常,err:%v", err)
		return
	}

	logger.Debugf("%v", rootNodePt)

	// 将链表转为数组，返回给前端

	transformPt2Arr(&resultArr, rootNodePt, "", "", bookIdx, 1)

	return
}

func transformPt2Arr(currentArr *[]*entity.TocStruct, currentNodePt *entity.TocStruct, noPrefix, path, bookIdx string, idx int) {
	if currentArr == nil || currentNodePt == nil {
		return
	}

	// logger.Infof("currentArr pt:%p", currentArr)

	noIdx := 1
	currentNodePt.SequenceNo = genNo(noPrefix, noIdx)
	noIdx++

	genPath(currentNodePt, path, bookIdx, idx)

	// 当前节点
	*currentArr = append(*currentArr, currentNodePt)

	// 兄弟节点
	siblingNodePt := currentNodePt.SiblingToc
	idxT := idx
	for siblingNodePt != nil {

		siblingNodePt.SequenceNo = genNo(noPrefix, noIdx)
		noIdx++

		idxT++
		genPath(siblingNodePt, path, bookIdx, idxT)

		*currentArr = append(*currentArr, siblingNodePt)
		(*siblingNodePt).Children = []*entity.TocStruct{}
		// logger.Infof("(*siblingNodePt).Children pt:%p", (*siblingNodePt).Children)

		transformPt2Arr(&(*siblingNodePt).Children, siblingNodePt.ChildToc, siblingNodePt.SequenceNo, siblingNodePt.Path, bookIdx, idx)

		siblingNodePt = siblingNodePt.SiblingToc
	}

	// 子节点
	currentNodePt.Children = []*entity.TocStruct{}
	// logger.Infof("currentNodePt.Children pt:%p", currentNodePt.Children)

	transformPt2Arr(&currentNodePt.Children, currentNodePt.ChildToc, currentNodePt.SequenceNo, currentNodePt.Path, bookIdx, 1)

}

func genNo(noPrefix string, noIdx int) (no string) {
	no = ""
	if len(noPrefix) > 0 {
		no = fmt.Sprintf("%s%d", noPrefix, noIdx)
	} else {
		no = fmt.Sprintf("%d", noIdx)
	}
	return
}

func genPath(pt *entity.TocStruct, path, bookIdx string, idx int) {
	if pt == nil {
		return
	}

	if len(path) <= 0 {
		pt.Path = fmt.Sprintf("%03s-%s", bookIdx, util.EscapeFileName(pt.Title))
	} else {
		pt.Path = filepath.Join(path, fmt.Sprintf("%03d-%s", idx, util.EscapeFileName(pt.Title)))
	}
}

// 令牌桶 map, key-md5(yqCookie), val-令牌桶
var tokenBucketMap = map[string]*tokenBucket.TokenBucket{}

func DownloadDocs(coverOldFiles bool, bookId int, categoryName, bookName,
	yqCookie string, tocsJson []byte) (tocs []*entity.TocStruct, err error) {

	err = nil

	if len(categoryName) <= 0 || bookId <= 0 ||
		len(bookName) <= 0 || len(tocsJson) <= 0 || len(yqCookie) <= 0 {
		logger.Errorf("参数异常")
		err = fmt.Errorf("参数异常")
		return
	}

	baseDownloadPath := filepath.Join(webEntity.GetTargetDir(), categoryName, bookName)

	if !coverOldFiles {
		// 检查下载目录中是否存在对应文件夹，存在则不处理，提示需要删除
		stat, _ := os.Stat(baseDownloadPath)
		if stat != nil && stat.IsDir() {
			// 存在目录，查询目录下是否存在文件
			files, _ := os.ReadDir(baseDownloadPath)
			if len(files) > 0 {
				logger.Errorf("知识库目录已存在并且目录不为空")
				err = errors.New("知识库目录已存在并且目录不为空")
				return
			}
		}
	}

	// 解析 json
	err = json.Unmarshal(tocsJson, &tocs)
	if err != nil {
		logger.Errorf("[DownloadDocs]json解析异常,err:%v", err)
		return
	}

	var tocNames []string
	for _, toc := range tocs {
		tocNames = append(tocNames, toc.Title)
	}
	logger.Infof("[DownloadDocs]开始下载知识库:[%s], tocs:[%v]", bookName, tocNames)

	// 获取令牌桶
	cookieMd5 := md5.Sum([]byte(yqCookie))
	tbKey := fmt.Sprintf("%X_%d", cookieMd5, time.Now().Unix())
	tb, tbOk := tokenBucketMap[tbKey]
	if !tbOk {
		// 一秒3个令牌
		tb = tokenBucket.Create(3, 1*time.Second)
		tokenBucketMap[tbKey] = tb
	}

	// waitGroup 计数器
	wg := sync.WaitGroup{}
	for _, toc := range tocs {
		// 遍历 toc 时限流
		tb.TryTakeOne()

		wg.Add(1)
		toc.Init()
		go func() {
			defer func() {
				err := recover()
				if err != nil {
					logger.Errorf("[DownloadDocs]处理[Title=%s]panic,err:%v", toc.Title, err)
				}
			}()
			logger.Debugf("[wg]ptr:%p, title:%s, handle Add", &wg, toc.Title)
			HandleEachToc(bookId, categoryName, yqCookie, toc, &wg)
		}()
	}

	doneChan := make(chan bool)
	go func() {
		wg.Wait()
		doneChan <- true
	}()
	// time.Sleep(1 * time.Second)

	select {
	case <-doneChan:
		logger.Infof("[DownloadDocs]下载完成")
	case <-time.After(3 * time.Minute):
		logger.Errorf("[DownloadDocs]下载超时")
		err = fmt.Errorf("下载超时")
	}

	return
}

// 处理每个 toc
func HandleEachToc(bookId int, categoryName, yqCookie string, toc *entity.TocStruct, wg *sync.WaitGroup) {
	defer func() {
		err := recover()
		if err != nil {
			logger.Errorf("[HandleEachToc]处理[Title=%s]panic,err:%v", toc.Title, err)
		}
		logger.Debugf("[wg]ptr:%p, title:%s, handle Done", wg, toc.Title)
		wg.Done()
	}()

	tocPath := toc.Path
	tocDownloadPath := filepath.Join(webEntity.GetTargetDir(), categoryName, tocPath)

	logger.Debugf("[HandleEachToc]下载路径:[%s]", tocDownloadPath)

	if toc.Type == "TITLE" {
		// 目录，创建目录
		stat, _ := os.Stat(tocDownloadPath)
		if stat == nil || !stat.IsDir() {
			err := os.MkdirAll(tocDownloadPath, os.ModePerm)
			if err != nil {
				logger.Errorf("[HandleEachToc]创建目录失败,path:%s,err:%v", tocDownloadPath, err)
				toc.AddHandleResult("创建目录失败", true)
				return
			}
			logger.Infof("[HandleEachToc]创建目录成功,path:%s", tocDownloadPath)
			toc.AddHandleResult("创建目录成功", false)
		} else {
			toc.AddHandleResult("目录已存在", true)
		}
		return
	}

	// 处理 DOC
	if len(toc.URL) <= 0 {
		logger.Errorf("[HandleEachToc]URL异常,toc:%v", toc)
		toc.AddHandleResult("URL异常", true)
		return
	}

	// 处理 DOC 时判断父级目录是否创建
	base := filepath.Base(tocDownloadPath)
	lastIdx := strings.LastIndex(tocDownloadPath, base)
	tocParentPath := tocDownloadPath
	if lastIdx > -1 {
		tocParentPath = tocDownloadPath[:lastIdx]
	}
	stat, _ := os.Stat(tocParentPath)
	if stat == nil || !stat.IsDir() {
		err := os.MkdirAll(tocParentPath, os.ModePerm)
		if err != nil {
			logger.Errorf("[HandleEachToc]创建目录失败,path:%s,err:%v", tocParentPath, err)
			toc.AddHandleResult("创建目录失败", true)
			return
		}
		logger.Infof("[HandleEachToc]创建目录成功,path:%s", tocParentPath)
		toc.AddHandleResult("创建目录成功", false)
	}

	// https://www.yuque.com/api/docs/{{book.toc[n].url}}
	// ?book_id={{book.id}}&include_contributors=true&include_like=true
	// &include_hits=true&merge_dynamic_data=false

	docUrl := fmt.Sprintf("https://www.yuque.com/api/docs/%s?book_id=%d"+
		"&include_contributors=true&include_like=true&include_hits=true"+
		"&merge_dynamic_data=false", toc.URL, bookId)

	fakeBrowser := &util.FakeBrowserStruct{
		Url: docUrl,
		Headers: http.Header{
			"Cookie": []string{yqCookie},
		},
	}

	respBytes, err := fakeBrowser.DoGet()
	if err != nil {
		logger.Errorf("[HandleEachToc]请求异常,toc:%v,err:%v", toc, err)
		toc.AddHandleResult("请求异常", true)
		return
	}

	singleDoc := entity.YuqueSingleDoc{}
	err = json.Unmarshal(respBytes, &singleDoc)
	if err != nil {
		logger.Errorf("[HandleEachToc]json解析异常,toc:%v,err:%v", toc, err)
		toc.AddHandleResult("json解析异常", true)
		return
	}

	singleDoc.DownloadTocPath = tocDownloadPath

	// 将 HTML 下载到本地
	htmlContent := singleDoc.Data.Content
	wg.Add(1)
	logger.Debugf("[wg]ptr:%p, title:%s, save html Add", wg, toc.Title)
	go func(tocDownloadPath, htmlContent string, toc *entity.TocStruct, wg *sync.WaitGroup) {
		defer func() {
			err := recover()
			if err != nil {
				logger.Errorf("[HandleEachToc]保存HTML异常,toc:%v,panic,err:%v", toc, err)
			}
			logger.Debugf("[wg]ptr:%p, title:%s, save html Done", wg, toc.Title)
			wg.Done()
		}()
		err = os.WriteFile(fmt.Sprintf("%s.html", tocDownloadPath), []byte(htmlContent), 0666)
		if err != nil {
			logger.Errorf("[HandleEachToc]保存HTML异常,toc:%v,err:%v", toc, err)
			toc.AddHandleResult("保存HTML异常", true)
			return
		}
		toc.AddHandleResult("保存HTML成功", false)
	}(tocDownloadPath, htmlContent, toc, wg)

	ctx := context.Background()
	ctx = context.WithValue(ctx, "yqCookie", yqCookie)
	mdContent, err := ParseSingleDoc(ctx, &singleDoc)
	if err != nil {
		logger.Errorf("[HandleEachToc]MD转换异常,toc:%v,err:%v", toc, err)
		toc.AddHandleResult("MD转换异常", true)
		return
	} else if mdContent == nil || mdContent.Len() <= 0 {
		logger.Errorf("[HandleEachToc]MD转换为空,toc:%v", toc)
		toc.AddHandleResult("MD转换为空", true)
		return
	}

	// markdown 格式文本
	mdContentStr := mdContent.String()
	wg.Add(1)
	logger.Debugf("[wg]ptr:%p, title:%s, save md Add", wg, toc.Title)
	go func(tocDownloadPath, mdContentStr string, toc *entity.TocStruct, wg *sync.WaitGroup) {
		defer func() {
			err := recover()
			if err != nil {
				logger.Errorf("[HandleEachToc]保存Markdown异常,toc:%v,panic,err:%v", toc, err)
			}
			logger.Debugf("[wg]ptr:%p, title:%s, save md Done", wg, toc.Title)
			wg.Done()
		}()
		err = os.WriteFile(fmt.Sprintf("%s.md", tocDownloadPath), []byte(mdContentStr), 0666)
		if err != nil {
			logger.Errorf("[HandleEachToc]保存Markdown异常,toc:%v,err:%v", toc, err)
			toc.AddHandleResult("保存Markdown异常", true)
			return
		}
		toc.AddHandleResult("保存Markdown成功", false)
	}(tocDownloadPath, mdContentStr, toc, wg)

}
